package edu.gatech.cs4235.group3;

import java.util.ResourceBundle;
import java.io.File;
import java.util.ArrayList;

import edu.gatech.cs4235.group3.action.OnePadDecryptAction;
import edu.gatech.cs4235.group3.action.OnePadEncryptAction;
import edu.gatech.cs4235.group3.action.SetupAction;
import edu.gatech.cs4235.group3.exception.OneTimePadException;
import edu.gatech.cs4235.group3.domain.Message;
import edu.gatech.cs4235.group3.Analyze-Crack.Cracker;

import sun.security.util.Resources;

/**
 * This is the main class for the OneTimePad application.
 * 
 * The application performs 3 different functions.
 * 
 * 1. The first function of this program is to encrypt a 80 column
 * by 50 row plain text file using a one-time-pad key.
 * 
 * 2. The second function of this program is to decrypt a 80 column
 * by 50 row cyphertext file using a one-time pad key.
 * 
 * 3. Given a directory of captured cyphertext, the program should
 * be able to analyze and figure out the one-time-pad used to encrypt
 * the cyphertext.
 * 
 * In order to perform function 3, we know that the cyphertexts are all
 * encrypted by the same one-time-pad and that the last column of the
 * pad is the space character.
 * 
 * All messages contain printable ASCII characters (including space character,
 * characters 32 - 126).
 * 
 * The encryption algorithm is as follows. Let the plain text character = P
 * and the one-time-pad key for that character = t.
 * 
 * Add the P + t using zero-shifted ASCII values:
 * 
 * P + t => (80 - 32) + (116 - 32) = 132
 * 
 * Mod by 95
 * 
 * 132 mod 95 = 37
 * 
 * Reshift the value to a valid ASCII character:
 * 
 * 37 + 32 = 69, or E.
 * 
 * To decrypt we do the opposite.
 * 
 * E - t => (69 - 32) - (116 - 32) = -47 
 * 
 * Mod by 95
 * 
 * -47 mod 95 = 48
 * 
 * Shift to a valid ASCII character:
 * 
 * 48 + 32 = 80, or P.
 * 
 * 
 * @author Jeffrey Starker
 *
 */
public class OneTimePad {
	
	final static boolean DEBUG = false;
	
	private final static String fileName = "dracula.txt";
	
	final static ResourceBundle RB = Resources.getBundle(OneTimePad.class.getName());

	/**
	 * The main program can be run using the following options:
	 * 
	 * -e filename.txt, filename.txt, ... - The -e flag can be followed by
	 * a comma separated list of files to encrypt.
	 * 
	 * -d filename.txt, filename.txt, ... - The -d flag can be followed
	 * by a comma separated list of files to be decrypted.
	 * 
	 * -p filename.txt, filename.txt, ... - The -p flag can be followed by
	 * a comma separated list of files pad files.
	 * 
	 * -r directory - The -r flag can be followed by a directory that contains
	 * cyphertext to be used to recover the one-time pads from.
	 * 
	 * -h - This displays the help for the program.
	 * 
	 * @param args
	 * @throws OneTimePadException 
	 */
	public static void main(String[] args) throws OneTimePadException {
		
		String encryptFiles = null;
		String decryptFiles = null;
		String padFiles = null;
		
		String recoverDir = null;
		
		if (DEBUG) {
			SetupAction setupAction = new SetupAction(fileName);
			setupAction.performAction();
		}
		
		if (args.length == 0) {
			printHelp();
			System.exit(0);
		}
		
		// Parse the arguments.
		for (int i = 0; i < args.length; i++) {
			String arg = args[i];
			
			// If we have the help flag, then simple print
			// the help and terminate immediately.
			if ("-h".equals(arg) || "-help".equals(arg)) {
				printHelp();
				System.exit(0);
				
			// Parse the e argument.
			} else if ("-e".equals(arg) && encryptFiles == null) {
				i++;
				encryptFiles = args[i];
				
		    // Parse the d argument.
			} else if ("-d".equals(arg) && decryptFiles == null) {
				i++;
				decryptFiles = args[i];
				
			// Parse the p argument.
			} else if ("-p".equals(arg) && padFiles == null) {
				i++;
				padFiles = args[i];
				
		    // Parse the r argument.
			} else if ("-r".equals(arg) && recoverDir == null) {
				i++;
				recoverDir = args[i];
				
			// If we get anything else, then we have an error and exit.
			} else {
				handleErrorAndExit("improperlyFormattedArguments");
			}
		}
		
		// If recoverDir is specified, then we need to recover keys.
		if (recoverDir != null) {
			Cracker keyGet = new Cracker();
			
			// we need all of the files to pass
			File passedDir = new File(recoverDir);
			String[] files = passedDir.list();
			
			// will hold the cipher text to feed in
			ArrayList<char[][]> text = new ArrayList<char[][]>(); 			
			Message parser;

			// get the cipher text by parsing it
			for (int i = 0; i < files.length; i++ ) {
				parser = new Message(files[i]);
				text.add(parser.getText());
			}
			
			// get the keys
			char[][][] key = keyGet.crackMany(text);	
			
			String dirName = "OTPs-Recovered";
			new File(dirName).mkdir();
			
			// file out the keys
			Message toFile;
			for(int i=0;i<key[0].length;i++){
				toFile.setText(key[i]);
				toFile.saveMessage(dirName + "/OTP_" + i);	
	                }
			
		} // 
		
		// If encryptFiles are not null, then we need to encrypt.
		if (encryptFiles != null) {
			
			OnePadEncryptAction action = new OnePadEncryptAction(encryptFiles, padFiles);
			
			try {
				action.performAction();
			} catch (final OneTimePadException exception) {
				handleErrorAndExit(exception);
			}
			
		} // End Encrypt Files action.
		
		// If decryptFiles are not null, then we need to decrypt.
		if (decryptFiles != null) {
			
			OnePadDecryptAction action = new OnePadDecryptAction(decryptFiles, padFiles);
			
			try {
				action.performAction();
			} catch (final OneTimePadException exception) {
				handleErrorAndExit(exception);
			}
		}
	}
	
	/**
	 * Handles an OneTimePadException error and exits.
	 *
	 * @param exception The exception to handle.
	 */
	public static void handleErrorAndExit(final OneTimePadException exception) {
		
		if (exception.getValues() == null) {
			handleErrorAndExit(exception.getKey());
		} else {
			handleErrorAndExit(exception.getKey(), exception.getValues());
		}
		
	}
	
	/**
	 * This method is responsible for printing errors using a keyName which retrieves a String
	 * from the resource bundle and then populates the message with the values given in args.
	 * 
	 * @param keyName The key name of the error message to print out.
	 * @param args A list of values to incorporate to the message.
	 */
	public static void handleError(final String keyName, Object...args) {
		
		String message = RB.getString(keyName);
		String populatedMessage = String.format(message, args);
		
		System.out.println(populatedMessage);
	}
	
	/**
	 * Does the same thing as {@link #handleError(String, Object...)} and it also exits the program.
	 * 
	 * @param keyName The name of the key used to retrieve the message.
	 * @param args The arguments used to populate the message.
	 */
	public static void handleErrorAndExit(final String keyName, Object...args) {
		handleError(keyName, args);
		System.exit(-1);
	}
	
	/**
	 * This function prints out the help and usage of the program.
	 * 
	 */
	public static void printHelp() {
		
		StringBuilder help = new StringBuilder();
		help.append("Usage: java OneTimePad [-h] [-e fileName,fileName,...] ");
		help.append("[-d fileName,fileName,...] [-p fileName,fileName,...] ");
		help.append("[-r directory]\n\n");
		
		help.append("h : help, displays this message.\n");
		help.append("e : encrypt the comma-seperated list of files following the flag\n");
		help.append("d : decrypt the comma-seperated list of files following the flag\n");
		help.append("p : uses the comma-seperated list of files as one-time-pads follwing the flag\n");
		help.append("r : uses the directory following the flag, try to recover a one-time pad from the cyphertexts contained in the directory\n");
	
		help.append("\n");
		help.append("If -e or -d are specified then there must be an same number of files specified with -p.");
		System.out.println(help.toString());
	}

}
